﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FluentETL.Impl;

namespace FluentETL.Impl
{
    public class Transform : ITransform
    {
        private Dictionary<string, string> _transformMap;
        private Dictionary<string, Func<object, object>> _transformFuncs;
        private ISource _src;
        private IDestination _dest;

        protected Action<IRow, RowValidation> DoRowValidation { get; set; }
        protected Func<IRow, RowOperation> GetRowOperation { get; set; }

        protected event Action<Transform> Start;
        protected event Action<Transform, Exception> Error;
        protected event Action<Transform> Complete;
        protected event Action<IRow, RowValidation> RowInvalid;
        protected event Action<IRow, RowOperation> RowBeforeProcess;
        protected event Action<IRow, RowOperation, Exception> RowError;
        protected event Action<IRow, RowOperation> RowSuccess;
        protected event Action<IRow, RowOperation> RowAfterProcess;

        public Transform(ISource src, IDestination dest)
        {
            _src = src;
            _dest = dest;
            _transformMap = new Dictionary<string, string>();
            _transformFuncs = new Dictionary<string,Func<object,object>>();
            this.DoRowValidation = (row, v) => { };
            this.GetRowOperation = row => RowOperation.Process;
        }

        public Transform AutoMap()
        {
            AutoMap(x =>
            {
                if (x != null)
                    return new DestinationMapping() { Field = x };
                return null;
            });
            return this;
        }

        public Transform AutoMap(Func<string, string> getMapping)
        {
            AutoMap(x =>
            {
                if (x != null)
                    return new DestinationMapping() { Field = getMapping(x) };
                return null;
            });
            return this;
        }

        public Transform AutoMap(Func<string, DestinationMapping> getMapping)
        {
            IFieldNames srcFieldNamesInfo = _src as IFieldNames;
            IFieldNames destFieldNamesInfo = _dest as IFieldNames;
            if (srcFieldNamesInfo == null)
                throw new InvalidOperationException("Cannot automap source.");
            else
            {
                var srcFields = srcFieldNamesInfo.GetFieldNames();
                var srcAndDestMappings = from f in srcFields
                                   let m = getMapping(f)
                                   where m != null
                                   select new KeyValuePair<string, DestinationMapping>(f, m);
                if (destFieldNamesInfo != null)
                {
                    // Find fields with match in destination
                    var destFields = destFieldNamesInfo.GetFieldNames();
                    var matchedMappings = from srcAndMap in srcAndDestMappings
                                          join f2 in destFieldNamesInfo.GetFieldNames() on srcAndMap.Value.Field equals f2
                                          select srcAndMap;
                    foreach (var srcAndMap in matchedMappings)
                    {
                        Map(srcAndMap.Key, srcAndMap.Value.Field, srcAndMap.Value.Transform);
                    }
                }
                else
                {
                    // Assume all fields have destinations
                    // i.e. match blindly, may cause errors depending on destination
                    foreach (var srcAndMap in srcAndDestMappings)
                    {
                        Map(srcAndMap.Key, srcAndMap.Value.Field, srcAndMap.Value.Transform);
                    }
                }
                return this;
            }
        }

        public Transform Map(string srcField)
        {
            _transformMap[srcField] = srcField;
            if (_transformFuncs.ContainsKey(srcField))
                _transformFuncs.Remove(srcField);
            return this;
        }

        public Transform Map(string srcField, string destField)
        {
            _transformMap[srcField] = destField;
            if (_transformFuncs.ContainsKey(srcField))
                _transformFuncs.Remove(srcField);
            return this;
        }

        public Transform Map(string srcField, string destField, Func<object, object> transformFunc)
        {
            _transformMap[srcField] = destField;
            if (transformFunc != null)
                _transformFuncs[srcField] = transformFunc;
            return this;
        }

        public Transform Map<T>(string srcField, string destField, Func<T, T> transformFunc)
        {
            _transformMap[srcField] = destField;
            if (transformFunc != null)
                _transformFuncs[srcField] = (x => transformFunc((T)x));
            return this;
        }

        public Transform Map<TOriginal, TTransformed>(string srcField, string destField, Func<TOriginal, TTransformed> transformFunc)
        {
            _transformMap[srcField] = destField;
            if (transformFunc != null)
                _transformFuncs[srcField] = (x => transformFunc((TOriginal)x));
            return this;
        }

        public Transform Unmap(string srcField)
        {
            if (_transformMap.ContainsKey(srcField))
                _transformMap.Remove(srcField);
            if (_transformFuncs.ContainsKey(srcField))
                _transformFuncs.Remove(srcField);
            return this;
        }

        public Transform Validate(Action<IRow, RowValidation> action)
        {
            this.DoRowValidation = action;
            return this;
        }

        public Transform DetermineOperation(Func<IRow, RowOperation> func)
        {
            this.GetRowOperation = func;
            return this;
        }

        public Transform OnStart(Action<Transform> action)
        {
            this.Start += action;
            return this;
        }

        public Transform OnError(Action<Transform, Exception> action)
        {
            this.Error += action;
            return this;
        }

        public Transform OnComplete(Action<Transform> action)
        {
            this.Complete += action;
            return this;
        }

        public Transform OnRowInvalid(Action<IRow, RowValidation> action)
        {
            this.RowInvalid += action;
            return this;
        }

        public Transform OnRowBeforeProcess(Action<IRow, RowOperation> action)
        {
            this.RowBeforeProcess += action;
            return this;
        }

        public Transform OnRowAfterProcess(Action<IRow, RowOperation> action)
        {
            this.RowAfterProcess += action;
            return this;
        }

        public Transform OnRowError(Action<IRow, RowOperation, Exception> action)
        {
            this.RowError += action;
            return this;
        }

        public Transform OnRowSuccess(Action<IRow, RowOperation> action)
        {
            this.RowSuccess += action;
            return this;
        }

        public Transform Execute()
        {
            if (_transformMap.Count == 0)
                throw new InvalidOperationException("No mappings defined.");

            if (this.Start != null)
                this.Start(this);

            try
            {
                _src.InTransformContext(this, () =>
                {
                    _dest.InTransformContext(this, () =>
                    {
                        foreach (var inputRow in _src.Rows)
                        {
                            RowValidation rowValidation = new RowValidation();
                            DoRowValidation(inputRow, rowValidation);
                            if (rowValidation.HasErrors)
                            {
                                if (this.RowInvalid != null)
                                    this.RowInvalid(inputRow, rowValidation);
                            }
                            else
                            {
                                RowOperation rowOp = this.GetRowOperation(inputRow);
                                if (rowOp != RowOperation.Ignore)
                                {
                                    DictionaryRow transformedRow = new DictionaryRow();
                                    foreach (var mapping in _transformMap)
                                    {
                                        object rowValue = inputRow[mapping.Key];
                                        Func<object, object> transformFunc;
                                        if (_transformFuncs.TryGetValue(mapping.Key, out transformFunc))
                                            rowValue = transformFunc(rowValue);
                                        transformedRow[mapping.Value] = rowValue;
                                    }
                                    ProcessTransformedRow(rowOp, transformedRow);
                                }
                            }
                        }
                    });
                });
            }
            catch (Exception exTransform)
            {
                if (this.Error != null)
                    this.Error(this, exTransform);
                else
                    throw exTransform;
            }

            if (this.Complete != null)
                this.Complete(this);

            return this;
        }

        private void ProcessTransformedRow(RowOperation rowOp, DictionaryRow transformedRow)
        {
            if (this.RowBeforeProcess != null)
                this.RowBeforeProcess(transformedRow, rowOp);

            try
            {
                switch (rowOp)
                {
                    case RowOperation.Process:
                        _dest.Process(transformedRow);
                        break;
                    case RowOperation.Insert:
                        _dest.Insert(transformedRow);
                        break;
                    case RowOperation.Update:
                        _dest.Update(transformedRow);
                        break;
                    case RowOperation.Delete:
                        _dest.Delete(transformedRow);
                        break;
                }

                if (this.RowSuccess != null && rowOp != RowOperation.Ignore)
                    this.RowSuccess(transformedRow, rowOp);
            }
            catch (Exception exRow)
            {
                if (this.RowError != null)
                    this.RowError(transformedRow, rowOp, exRow);
                else
                    throw exRow;
            }

            if (this.RowAfterProcess != null)
                this.RowAfterProcess(transformedRow, rowOp);
        }

        IEnumerable<string> ITransform.GetMappedSourceFields()
        {
            return _transformMap.Keys;
        }

        IEnumerable<string> ITransform.GetMappedDestinationFields()
        {
            return _transformMap.Values;
        }

        void ITransform.Execute()
        {
            this.Execute();
        }
    }
}
